// ================================================================================
//
//      作 者  :   G Q
//      类 名  :   LoopList
//      时 间  :   2022年11月29日 14:42:30
//      目 的  :   实现可复用的无限循环列表
//
// ================================================================================

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Axis = UnityEngine.RectTransform.Axis;
using Corner = UnityEngine.UI.GridLayoutGroup.Corner;

public class LoopList : MonoBehaviour
{
    /// <summary>
    /// Item模板
    /// </summary>
    [SerializeField]
    private GameObject template;

    /// <summary>
    /// 显示（滑动）区域 对应 Viewport
    /// </summary>
    [SerializeField]
    private RectTransform viewport;

    /// <summary>
    /// 内容区域（放置Item的父节点） 对应 Content
    /// </summary>
    [SerializeField]
    private RectTransform content;
    
    /// <summary>
    /// Padding
    /// </summary>
    [SerializeField]
    private RectOffset padding;

    /// <summary>
    /// Item间隔
    /// </summary>
    [SerializeField]
    private Vector2 spacing;
    
    /// <summary>
    /// 起始角（左上，左下，右上，右下）
    /// </summary>
    [SerializeField]
    private Corner startCorner;
    
    /// <summary>
    /// 起始轴（水平，垂直）
    /// </summary>
    [SerializeField]
    private Axis startAxis;

    /// <summary>
    /// Item对齐方式
    /// </summary>
    [SerializeField]
    private TextAnchor childAlignment;


    /// <summary>
    /// 数据dataList
    /// </summary>
    private List<object> dataList = new List<object>();

    /// <summary>
    /// 对象池列表
    /// </summary>
    private List<GameObject> poolList = new List<GameObject>();

    /// <summary>
    /// 用来存储当前显示的Item列表
    /// </summary>
    private Dictionary<int, GameObject> showPoolList = new Dictionary<int, GameObject>();

    /// <summary>
    /// 显示区域（viewport）大小
    /// </summary>
    private Rect viewportRect;

    /// <summary>
    /// 是否已经初始化？
    /// </summary>
    private bool isInit;

    /// <summary>
    /// 内容区域起始位置
    /// </summary>
    private Vector3 contentStartPos;
    /// <summary>
    /// 此次滑动大小数值
    /// </summary>
    private Vector2 scrollOffset;
    /// <summary>
    /// 上次滑动大小数值
    /// </summary>
    private Vector2 lastScrollOffset;

    /// <summary>
    /// Item刷新函数（必传）
    /// </summary>
    private Action<GameObject, LoopListItem> updateFunc;
    /// <summary>
    /// Item事件注册函数（可为null）
    /// </summary>
    private Action<GameObject, LoopListItem> enableFunc;
    /// <summary>
    /// Item隐藏调用函数（可为null）
    /// </summary>
    private Action<GameObject, LoopListItem> disableFunc;

    private void OnValidate()
    {
        UpdateViewportRect();
        UpdateContentSize();
    }

    private void LateUpdate()
    {
        if (dataList == null || dataList.Count == 0)
        {
            return;
        }

        scrollOffset = content.localPosition - contentStartPos;
        if (scrollOffset != lastScrollOffset)
        {
            // 此次滑动数量
            Vector2 scrollVector = scrollOffset - lastScrollOffset;
            //Debug.LogError($"{scrollOffset}, {lastScrollOffset}, {scrollVector}");
            var itemSizeDelta = GetItemSizeDelta();
            var curX = 0;
            var curY = 0;
            var maxX = GetMaxXCount(content.sizeDelta);
            var maxY = GetMaxYCount(content.sizeDelta);
            var offsetIndex = 0;

            if (startAxis == Axis.Horizontal)
            {
                curY = Mathf.CeilToInt(Mathf.Abs(scrollVector.y) / itemSizeDelta.y);
                offsetIndex = curY * maxX;
            }
            else if (startAxis == Axis.Vertical)
            {
                curX = Mathf.CeilToInt(Mathf.Abs(scrollVector.x) / itemSizeDelta.x);
                offsetIndex = curX * maxY;
            }

            lastScrollOffset = scrollOffset;
            UpdateShowItemList();
        }
    }

    public void InitLoopList(Action<GameObject, LoopListItem> updateFunc, Action<GameObject, LoopListItem> enableFunc = null, Action<GameObject, LoopListItem>  disableFunc = null)
    {
        this.updateFunc = updateFunc;
        this.enableFunc = enableFunc;
        this.disableFunc = disableFunc;
    }

    /// <summary>
    /// 设置无限滚动列表数据（主入口）
    /// </summary>
    /// <typeparam name="T">泛型</typeparam>
    /// <param name="dataList">数据列表</param>
    public void SetDataList<T>(List<T> dataList)
    {
        if (!CheckCondition())
        {
            // 必要条件检查不过，直接返回
            return;
        }

        if (dataList == null || dataList.Count <= 0)
        {
            Debug.LogError($"设置数据为空或数据长度为0，请检查!!");
            return;
        }

        this.dataList.Clear();
        dataList.ForEach(item => this.dataList.Add(item));

        // 更新 Viewport 显示区域（滑动区域）大小
        UpdateViewportRect();

        // 更新 Content 内容区域大小
        UpdateContentSize();

        // 更新 Content 起始位置
        UpdateStartPos();

        // 更新 对象池 数据
        UpdateSpawnPool();

        // 更新 滚动列表 显示数据
        UpdateShowItemList();
    }

    public void UpdateView()
    {
        foreach (var showItem in showPoolList)
        {
            this.updateFunc?.Invoke(showItem.Value, new LoopListItem(showItem.Key, dataList[showItem.Key]));
            this.enableFunc?.Invoke(showItem.Value, new LoopListItem(showItem.Key, dataList[showItem.Key]));
        }
    }


    /// <summary>
    /// 检查必要条件
    /// </summary>
    /// <returns></returns>
    private bool CheckCondition()
    {
        if (template == null)
        {
            Debug.LogError($"预制体模板Item为空，请检查!!");
            return false;
        }

        if (viewport == null)
        {
            Debug.LogError($"滑动区域为空，请检查!!");
            return false;
        }

        if (content == null)
        {
            Debug.LogError($"内容区域为空，请检查!!");
            return false;
        }

        return true;
    }

    /// <summary>
    /// 更新 Viewport 显示区域（滑动区域）大小
    /// </summary>
    private void UpdateViewportRect()
    {
        // 获取滑动区域四个角的坐标（顺序为顺时针方向：依次从左下，左上，右上，右下顺序，如下图）
        /*
         *      1 ----- 2
         *      |       |
         *      |       |
         *      0 ----- 3
         */
        Vector3[] worldCorners = new Vector3[4];
        viewport.GetWorldCorners(worldCorners);
        // 左下角
        Vector3 leftLowerCorner = content.InverseTransformPoint(worldCorners[0]);
        // 右上角
        Vector3 rightTopCorner = content.InverseTransformPoint(worldCorners[2]);
        // 显示区域大小
        viewportRect = new Rect(leftLowerCorner, rightTopCorner - leftLowerCorner);
    }

    /// <summary>
    /// 更新 Content 滑动区域大小
    /// </summary>
    private void UpdateContentSize()
    {
        int dataCount = GetDataListCount();
        Vector2 sizeDelta = Vector2.zero;
        Vector2 itemSizeDelta = GetItemSizeDelta();
        if (startAxis == Axis.Horizontal)
        {
            // 水平排列
            // 算出剩余像素水平方位最多能放置多少个Item
            // TODO GQ 计算Content的大小
            var maxCountX = GetMaxXCount(viewportRect.size);
            sizeDelta.x = viewportRect.size.x;
            // 计算当前数量的Item需要生成多高（sizeDelta.y）的内容区域
            sizeDelta.y = (dataCount / maxCountX + (dataCount % maxCountX == 0 ? 0 : 1)) * (itemSizeDelta.y + spacing.y) + padding.top + padding.bottom;
        }
        else if (startAxis == Axis.Vertical)
        {
            // 垂直排列
            // 算出剩余像素垂直方位最多能放置多少个Item
            // TODO GQ 计算Content的大小
            var maxCountY = GetMaxYCount(viewportRect.size);
            // 计算当前数量的Item需要生成多宽（sizeDelta.x）的内容区域
            sizeDelta.x = (dataCount / maxCountY + (dataCount % maxCountY == 0 ? 0 : 1)) * (itemSizeDelta.x + spacing.x) + padding.left + padding.right;
            sizeDelta.y = viewportRect.size.y;
        }

        Vector2 anchor = new Vector2(GetAlignmentOnAxis(0), 1 - GetAlignmentOnAxis(1));
        content.pivot = anchor;
        content.anchorMin = anchor;
        content.anchorMax = anchor;
        content.sizeDelta = sizeDelta;
    }

    /// <summary>
    /// 更新 Content 起始位置
    /// </summary>
    private void UpdateStartPos()
    {
        if (!isInit)
        {
            isInit = true;
            contentStartPos = content.localPosition;
        }
        else
        {
            scrollOffset = Vector2.zero;
            lastScrollOffset = Vector2.zero;
            content.localPosition = contentStartPos;
        }
    }

    /// <summary>
    /// 更新 对象池 数据
    /// </summary>
    private void UpdateSpawnPool()
    {
        // 模板Item大小
        var sizeDelta = GetItemSizeDelta();
        // 显示区域大小
        var showSize = viewportRect.size;

        var left = padding.left;
        var right = padding.right;
        // 计算除开左右偏移后还剩余多少像素
        var surplusX = (int)showSize.x - left - right;
        // 一排（水平方向）最多显示多少个
        //var maxShowX = Mathf.FloorToInt(surplusX / sizeDelta.x);
        var maxShowX = GetMaxXCount(showSize);

        var top = padding.top;
        var bottom = padding.bottom;
        // 计算除开上下偏移后还剩余多少像素
        var surplusY = (int)showSize.y - top - bottom;
        // 一列（垂直方向）最多显示多少个
        //var maxShowY = Mathf.FloorToInt(surplusY / sizeDelta.y);
        var maxShowY = GetMaxYCount(showSize);

        // 由于需要循环复用，所以需要加载一些多余的模板Item以作复用（目的：防止滑动时没有多余的Item复用导致加载新Item卡顿）
        var showMoreCount = startAxis == Axis.Horizontal ? maxShowX : (startAxis == Axis.Vertical ? maxShowY : 0);
        // 当前对象池显示数量 = 行 * 列 + 多余个数 * 2
        var poolCount = maxShowX * maxShowY + (showMoreCount + 1) * 2;
        // 取 对象池数量 与 总数据长度 的最小值
        poolCount = Mathf.Min(poolCount, GetDataListCount());

        // 更新对象池数据之前，先把当前显示的对象池列表清空
        ClearShowPoolList();

        template.SetActive(false);
        GameObject item;
        // 当对象池数量不够的时候，再实例化
        var initPoolCount = poolList.Count;
        for (int i = poolCount; i > initPoolCount; i--)
        {
            item = Instantiate(template, content);
            var itemRectTransform = item.GetComponent<RectTransform>();
            //itemRectTransform.anchorMax = Vector2.up;
            //itemRectTransform.anchorMin = Vector2.up;
            //itemRectTransform.sizeDelta = sizeDelta;
            poolList.Add(item);
        }
    }

    /// <summary>
    /// 更新 滚动列表 显示数据
    /// </summary>
    private void UpdateShowItemList()
    {
        // 获取 真实行列（把数据全部显示完全所需要的行列） 数量（整个 Content内容区域）
        GetRealXAndY(out int realX, out int realY);
        // 模板大小
        var itemSizeDelta = GetItemSizeDelta();
        // Content内容区域 的真实大小
        var contentRealSize = new Vector2(realX * itemSizeDelta.x, realY * itemSizeDelta.y);
        var v2 = new Vector2(GetStartOffset(0, contentRealSize.x), GetStartOffset(1, contentRealSize.y));
        // 滑动区域一行最多显示数量
        var limitX = GetMaxXCount(viewportRect.size);
        // 滑动区域一列最多显示数量
        var limitY = GetMaxYCount(viewportRect.size);
        // 当前行
        var curX = 0;
        // 当前列
        var curY = 0;
        // 总数据长度
        var dataListCount = GetDataListCount();

        RectTransform itemRect = null;
        for (int i = 0; i < dataListCount; i++)
        {
            if (showPoolList.ContainsKey(i))
            {
                itemRect = showPoolList[i].GetComponent<RectTransform>();
            }
            else
            {
                if (poolList.Count <= 0)
                {
                    // 防止滑动过快，导致需要加载新的Item，但poolList对象池里却没有多余的咯
                    continue;
                }

                itemRect = poolList[0].GetComponent<RectTransform>();
            }

            if (startAxis == Axis.Horizontal)
            {
                curX = i % limitX;
                curY = i / limitX;
            }
            else if (startAxis == Axis.Vertical)
            {
                curY = i % limitY;
                curX = i / limitY;
            }

            var posX = v2.x + (itemSizeDelta.x + spacing.x) * curX;
            var posY = v2.y + (itemSizeDelta.y + spacing.y) * curY;

            itemRect.anchorMax = Vector2.up;
            itemRect.anchorMin = Vector2.up;
            itemRect.sizeDelta = itemSizeDelta;

            posX += (((int) startCorner & 1) > 0) ? padding.right : padding.left;
            posY += (((int) startCorner & 2) > 0) ? padding.bottom : padding.top;

            SetChildAlongAxis(itemRect, 0, posX, itemSizeDelta.x);
            SetChildAlongAxis(itemRect, 1, posY, itemSizeDelta.y);

            Rect rect = Rect.zero;
            rect.min = (Vector2)itemRect.localPosition - itemRect.pivot * itemRect.sizeDelta;
            rect.size = itemRect.sizeDelta;
            rect.position += scrollOffset;
            var isOverLaps = viewportRect.Overlaps(rect);
            if (isOverLaps && !showPoolList.ContainsKey(i))
            {
                AddSpawn(i);
            }
            else if (!isOverLaps && showPoolList.ContainsKey(i))
            {
                RemoveSpawn(i);
            }
        }
    }

    /// <summary>
    /// 添加Item到显示列表
    /// </summary>
    /// <param name="index"></param>
    private void AddSpawn(int index)
    {
        var item = poolList[0];
        if (item == null)
        {
            return;
        }
        showPoolList.Add(index, item);
        poolList.RemoveAt(0);

        item.SetActive(true);
        item.name = "Item_" + index;

        // TODO GQ 当显示了Item后，需要刷新Item数据
        LoopListItem loopListItem = new LoopListItem(index, dataList[index]);
        this.updateFunc?.Invoke(item, loopListItem);
        this.enableFunc?.Invoke(item, loopListItem);
    }

    /// <summary>
    /// 从显示列表移除Item
    /// </summary>
    /// <param name="index"></param>
    private void RemoveSpawn(int index)
    {
        var item = showPoolList[index];
        showPoolList.Remove(index);
        poolList.Add(item);

        item.SetActive(false);

        LoopListItem loopListItem = new LoopListItem(index, dataList[index]);
        this.disableFunc?.Invoke(item, loopListItem);
    }

    /// <summary>
    /// 清理当前展示的Item列表
    /// </summary>
    private void ClearShowPoolList()
    {
        foreach (var showItem in showPoolList)
        {
            showItem.Value.SetActive(false);
            poolList.Add(showItem.Value);
        }
        showPoolList.Clear();
    }


    /// <summary>
    /// 获得模板Item的大小
    /// </summary>
    /// <returns>模板Item的sizeDelta</returns>
    private Vector2 GetItemSizeDelta()
    {
        if (template == null)
        {
            Debug.LogError($"预制体模板Item为空，请检查!!");
            return Vector2.zero;
        }

        return template.GetComponent<RectTransform>().sizeDelta;
    }

    /// <summary>
    /// 获得数据长度（外界通过调用 SetDataList 时传入的数据）
    /// </summary>
    /// <returns>长度</returns>
    private int GetDataListCount()
    {
        return dataList.Count;
    }

    /// <summary>
    /// 根据区域大小判断一行做到放置多少个模板Item
    /// </summary>
    /// <param name="showSizeDelta">区域大小</param>
    /// <returns>水平方向上放置的最大模板数量</returns>
    private int GetMaxXCount(Vector2 showSizeDelta)
    {
        var cellSize = GetItemSizeDelta();
        var sizeDeltaX = Mathf.CeilToInt(showSizeDelta.x);
        var left = padding.left;
        var right = padding.right;
        // 计算除开左右偏移后还剩余多少像素（这里不能减right，因为Item元素从左以此往右摆，right是在元素摆完后，最右边的元素距离右边还剩right个像素）
        var surplusX = (int) sizeDeltaX - left;// - right;
        // 算出剩余像素水平方位最多能放置多少个Item
        var maxCountX = surplusX / Mathf.CeilToInt((cellSize.x + spacing.x));
        maxCountX = startAxis == Axis.Vertical ? maxCountX + 1 : maxCountX;

        return maxCountX;
    }

    /// <summary>
    /// 根据区域大小判断一列最多放置多少个模板Item
    /// </summary>
    /// <param name="showSizeDelta">区域大小</param>
    /// <returns>垂直方向上放置的最大模板数量</returns>
    private int GetMaxYCount(Vector2 showSizeDelta)
    {
        var cellSize = GetItemSizeDelta();
        var sizeDeltaY = Mathf.CeilToInt(showSizeDelta.y);
        var top = padding.top;
        var bottom = padding.bottom;
        // 计算除开上下偏移后还剩余多少像素（这里不能减bottom，因为Item元素从上以此往下摆，bottom是在元素摆完后，最下面的元素距离底部还剩bottom个像素）
        var surplusY = (int) sizeDeltaY - top;// - bottom;
        // 算出剩余像素垂直方位最多能放置多少个Item
        var maxCountY = surplusY / Mathf.CeilToInt((cellSize.y + spacing.y));
        maxCountY = startAxis == Axis.Horizontal ? maxCountY + 1 : maxCountY;

        return maxCountY;
    }

    /// <summary>
    /// 获取内容区域真正能放置的行列数量
    /// </summary>
    /// <param name="realX">水平方向真正能放置的数量</param>
    /// <param name="realY">垂直方向真正能放置的数量</param>
    private void GetRealXAndY(out int realX, out int realY)
    {
        if (startAxis == Axis.Horizontal)
        {
            realX = GetMaxXCount(content.sizeDelta);
            realY = Mathf.CeilToInt(GetDataListCount() / (float) realX);
        }
        else// if (startAxis == Axis.Vertical)
        {
            realY = GetMaxYCount(content.sizeDelta);
            realX = Mathf.CeilToInt(GetDataListCount() / (float)realY);
        }
    }

    /// <summary>
    /// 这是 LayoutGroup 里的方法，直接抄过来的
    /// 获得子物体对齐方式
    /// </summary>
    /// <param name="axis"></param>
    /// <returns></returns>
    private float GetAlignmentOnAxis(int axis)
    {
        if (axis == 0)
            return (float)((int)this.childAlignment % 3) * 0.5f;
        return (float)((int)this.childAlignment / 3) * 0.5f;
    }

    /// <summary>
    /// 这是 LayoutGroup 里的方法，直接抄过来的
    /// 获取第一个子物体沿给定轴的计算位置
    /// </summary>
    /// <param name="axis">轴：0：水平。1：垂直</param>
    /// <param name="requiredSpaceWithoutPadding"></param>
    /// <returns></returns>
    private float GetStartOffset(int axis, float requiredSpaceWithoutPadding)
    {
        float num1 = requiredSpaceWithoutPadding;
        float num2 = viewport.rect.size[axis] - num1;
        float alignmentOnAxis = GetAlignmentOnAxis(axis);
        return num2 * alignmentOnAxis;
    }

    /// <summary>
    /// 这是 LayoutGroup 里的方法，直接抄过来的
    /// 沿给定轴设置子物体的位置和大小。
    /// </summary>
    /// <param name="rect"></param>
    /// <param name="axis"></param>
    /// <param name="pos"></param>
    /// <param name="size"></param>
    private void SetChildAlongAxis(RectTransform rect, int axis, float pos, float size)
    {
        if ((UnityEngine.Object)rect == (UnityEngine.Object)null)
            return;
        rect.SetInsetAndSizeFromParentEdge(axis != 0 ? RectTransform.Edge.Top : RectTransform.Edge.Left, pos, size);
    }
}

/// <summary>
/// 无限循环列表的Item元素数据
/// </summary>
public class LoopListItem
{
    /// <summary>
    /// 索引：当前是第几个Item
    /// </summary>
    public int index;

    /// <summary>
    /// 当前Item所对应的数据
    /// </summary>
    public object data;

    /// <summary>
    /// 构造
    /// </summary>
    /// <param name="index">索引</param>
    /// <param name="data">索引对应的数据</param>
    public LoopListItem(int index, object data)
    {
        this.index = index;
        this.data = data;
    }
}